//James' Pass:		zS9AZ3gS8bR2
//Elliot's Pass:	Aa6zd6Bh7VG4
//Alex's Pass:		aw4zF7mc9Gv9

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JOptionPane;


public class GarbooMain extends JFrame{



	boolean WEIRD_FRAMELESSNESS=true;

	//General Engine variabless
	final int _FRAMERATE = 59; //Target framerate, what we want it to be close to
	int targetNsPerFrame = (int)(1000 * 1e6/ (double)_FRAMERATE) ;
	final int FRAMERATE = (int)(1000* 1e6 /(double)targetNsPerFrame);  //Actual framerate, derived from _FRAMERATE

	final int WIDTH = 1280, HEIGHT = 800 + 80;
	final int PWIDTH = 32, PHEIGHT = 32;

	final String LEVELDIR = "Levels/";
	final String FIRSTLEVEL = "11";
	final String FIRSTTRAININGLEVEL = "01";
	final static int PIXELS_PER_METER = 15;
	final int FG = 40; //meters

	int xoff, yoff;  //Offsets for dealing with the window borders for drawing
	Font gFont_big, gFont_small;


	Image menu_img, credits_img, options_img, player_img_L, player_img_R,mp_vert,mp_horiz,
	turret_green, turret_red, bullet_img, endofgame, endofgame_training, selected_img, unselected_img,
	gpush40_img, gpush80_img, gpush120_img, gpush160_img, gpush200_img,
	gpull40_img,gpull80_img,gpull120_img,gpull160_img,gpull200_img ;





	HashMap<Integer, Image> laserimgs;
	AnimImage splatAnim, selector_img;
	final int SPLATFRAME_WIDTH = 80;
	final int SELECTOR_WIDTH = 6;

	//Modes
	Mode garbooMode;
	PauseMode pauseMode;

	//Listeners
	MenuListeners mListener;
	CreditsScreenListeners cListener;
	OptionsScreenListeners oListener;
	InGameListeners igListener;
	PauseListeners pListener;
	EndOfGameListener eogListener;


	//Level-specific variables
	Level currentLevel;
	Player player;
	DrawManager drawManager;

	ArrayList<Bullet> bullets; //Keep track of bullets produced by turrets
	Bullet[]blankBulletArray = new Bullet[0];

	ArrayList<Integer> tilesToCheck;
	Rectangle exitRect;
	boolean playingTrainingLevels;
	boolean funlevel = false;


	//Menu vars
	final int menu_start_x = 190;
	final int menu_start_y = 422;
	final int menu_item_dy = 43;
	final int menuItems = 6;
	int menuSelection = 0;

	int creditsScrollY = 0; //noscroll at first
	int scrollStartDelay = 1; //seconds
	int scrollPixelsPerFrame = 1; //px: 800px in 5 seconds (one full vertical screen)
	boolean scrollingCredits = true;


	MenuStructure optionsMenu;
	Node graphics, low, high, bouncing, noob, pro, menu;

	boolean high_graphics = true;
	boolean pro_bouncing = true;


	Color pauseScreenBGColor = new Color( .2f, .2f, .2f, .6f);
	BufferedImage blurredPause = GaussianBlur.createCompatibleImage(WIDTH, HEIGHT);

	Color laserWallColor_off = new Color( .5f, .5f, .5f, .6f);
	Color endOfGameBGColor = new Color( .2f, .2f, .2f, .9f);
	Color alphaBlack = new Color( 0f, 0f, 0f, .5f);

	public GarbooMain(){


		drawManager = new DrawManager();
		buildOptionsMenu();

		//Load images
		try {
			endofgame = ImageIO.read(new File("eog.png"));
			endofgame_training = ImageIO.read(new File("eog_training.png"));

			menu_img = ImageIO.read(new File("menuimg.png"));

			//help pics
			credits_img= ImageIO.read(new File("credits.png"));
			//options image
			options_img = ImageIO.read(new File("options.png"));

			//moving platform pics
			mp_vert = ImageIO.read(new File(LEVELDIR + "moving_v.png"));
			mp_horiz= ImageIO.read(new File(LEVELDIR + "moving_h.png"));
			//player pics L/R
			player_img_L = ImageIO.read(new File(LEVELDIR + "player_l.png"));
			player_img_R = ImageIO.read(new File(LEVELDIR + "player_r.png"));
			//turret in both tracking/not tracking states
			turret_green = ImageIO.read(new File(LEVELDIR+"turret_green.png"));
			turret_red= ImageIO.read(new File(LEVELDIR+"turret_red.png"));
			bullet_img = ImageIO.read(new File(LEVELDIR + "bullet.png"));


			//gravwell imgs
			gpush200_img = ImageIO.read(new File(LEVELDIR + "grav_push_200.png"));
			gpush160_img = ImageIO.read(new File(LEVELDIR + "grav_push_160.png"));
			gpush120_img = ImageIO.read(new File(LEVELDIR + "grav_push_120.png"));
			gpush80_img = ImageIO.read(new File(LEVELDIR + "grav_push_80.png"));
			gpush40_img = ImageIO.read(new File(LEVELDIR + "grav_push_40.png"));

			gpull200_img = ImageIO.read(new File(LEVELDIR + "grav_well_200.png"));
			gpull160_img = ImageIO.read(new File(LEVELDIR + "grav_well_160.png"));
			gpull120_img = ImageIO.read(new File(LEVELDIR + "grav_well_120.png"));
			gpull80_img = ImageIO.read(new File(LEVELDIR + "grav_well_80.png"));
			gpull40_img = ImageIO.read(new File(LEVELDIR + "grav_well_40.png"));




			laserimgs = new HashMap<Integer, Image>();
			laserimgs.put(0, ImageIO.read(new File( LEVELDIR + "laser70.png")));
			laserimgs.put(1, ImageIO.read(new File( LEVELDIR + "laser74.png")));
			laserimgs.put(2, ImageIO.read(new File( LEVELDIR + "laser78.png")));
			laserimgs.put(3, ImageIO.read(new File( LEVELDIR + "laser82.png")));

			splatAnim = new AnimImage(ImageIO.read(new File( LEVELDIR + "splatframes.png")), SPLATFRAME_WIDTH, FRAMERATE);

			//Options menu selected/unselected options pics
			selected_img = ImageIO.read(new File("selected.png"));
			unselected_img = ImageIO.read(new File("unselected.png"));

			//Menu 'currently active selection' indicator
			selector_img= new AnimImage( ImageIO.read(new File( "selector_sprite.png" )), SELECTOR_WIDTH, FRAMERATE);
			selector_img.setLooping(true);

		} catch (IOException e) {
			JOptionPane.showMessageDialog(null, "Could not load images.");
			System.err.println("Could not read some images....");
			e.printStackTrace();
			System.exit(0);
		}

		//Load the Garboo font
		try {		
			Font gFont;
			gFont = Font.createFont(Font.TRUETYPE_FONT, new FileInputStream(new File("garboo.ttf")));
			gFont_big = gFont.deriveFont(60f);
			gFont_small = gFont.deriveFont(20f);
		} catch (Exception e) {
			System.err.println("Could not load \"garboo.ttf\".");
			e.printStackTrace();
			System.exit(0);  //must exit since the fonts and the derivatives have NOT been defined properly...
		}





		//Initialize the window



		setResizable(false);
		setAlwaysOnTop(true);

		setUndecorated(WEIRD_FRAMELESSNESS);
		//		  GraphicsEnvironment.
		//	      getLocalGraphicsEnvironment().
		//	         getDefaultScreenDevice().setFullScreenWindow(this);


		setVisible(true);

		GraphicsConfiguration gc = this.getGraphicsConfiguration();
		Rectangle gc_r = gc.getBounds();
		this.setLocation(gc_r.width/2 - WIDTH/2, gc_r.height/2 - HEIGHT/2);

		xoff = getInsets().left;
		yoff = getInsets().top;

		setSize(     //Set the size to adjust for window borders
				WIDTH+      getInsets().left + getInsets().right,
				HEIGHT + getInsets().top + getInsets().bottom);

		setDefaultCloseOperation(EXIT_ON_CLOSE);

		//Create the buffer strategy, which is what gives is (near) instant frame updates.

		this.createBufferStrategy(2);
		this.setIgnoreRepaint(true);
		//Frame updating thread:
		Thread frameThread = new Thread(new FrameDrawer(this));
		frameThread.setPriority(Thread.MAX_PRIORITY);
		frameThread.start();

	}
	public void buildOptionsMenu(){
		optionsMenu = new MenuStructure();



		graphics = new Node("graphics", new Point( 147 , 199+67 ));
		low= new Node("low", new Point( 368 , 314 ));
		high= new Node("high", new Point( 601 , 314));

		bouncing = new Node("bouncing", new Point( 147 , 448+66 ));
		noob = new Node("noob", new Point( 368 , 498+68 ));
		pro = new Node("pro", new Point( 601 , 498+68 ));

		menu =  new Node("menu", new Point( 481-5 , 773+11 )); //special node




		graphics.setUp( menu );
		graphics.setDown(bouncing);
		graphics.setRight(low);

		bouncing.setUp( graphics);
		bouncing.setRight( noob );
		bouncing.setDown( menu);

		menu.setDown(graphics);
		menu.setUp(bouncing);


		low.setRight(high);
		low.setLeft(graphics);

		high.setLeft(low);


		noob.setLeft(bouncing);
		noob.setRight(pro);

		pro.setLeft(noob);

		optionsMenu.setHeadNode(graphics); //mark the starting location for the menu
		optionsMenu.setCurrentNode(graphics); //set starting location here

	}


	class FrameDrawer implements Runnable{
		GarbooMain game;

		BufferStrategy buffStrat;
		Graphics g;


		public FrameDrawer( GarbooMain game){

			this.game = game;
			buffStrat = game.getBufferStrategy();
			//Initialize all listeners
			igListener= new InGameListeners();
			mListener = new MenuListeners();
			cListener = new CreditsScreenListeners();
			oListener = new OptionsScreenListeners();
			pListener = new PauseListeners();
			eogListener = new EndOfGameListener();

			//Set the intial state of Garboo, i.e. menu and whatnot.

			setListeners(mListener);
			garbooMode = Mode.MAINMENU;
			pauseMode = PauseMode.CLICK_TO_BEGIN;

		}

		long lastFrameTimeStamp;

		public void run() {

			while(true){
				lastFrameTimeStamp =  System.nanoTime();

				//Update reference to the Graphics object:
				g = buffStrat.getDrawGraphics();

				//Update state if needed:
				updateState();

				//Draw whatever we're supposed to be drawing
				paint(g);

				//so i threw it in the TRAAAASH
				g.dispose();

				try {
					long slp = (long)targetNsPerFrame - (System.nanoTime() - lastFrameTimeStamp);

					if(slp >=0){
						Thread.sleep(Math.round(slp*1e-6));
					}else{
						//TODO: Modify dT to help catch up? :D
					}
				} catch (InterruptedException e) {/*Couldn't sleep for some reason....*/}

				buffStrat.show();
				Toolkit.getDefaultToolkit().sync();

			}
		}



		public void updateState(){
			//Selects which method to run to do any state-updates before redrawing screen
			//Since the only one that does anything besides call paint<Thing> is the INGAME
			//case, we could just get rid of everything except for aP_game(e)
			switch(garbooMode){
			case INGAME:
				uS_game();
				break;
			case MAINMENU:
				uS_menu();
				break;
			case CREDITS:
				uS_credits();
				break;
			case OPTIONS:
				uS_options();
				break;
			case PAUSE:
				uS_pause();
				break;
			case ENDOFGAME:
				uS_eog();
				break;
			}
		}
		public void uS_menu(){
		}
		public void uS_credits(){
		}
		public void uS_options(){
		}
		public void uS_pause(){
		}
		public void uS_game() {

			double fnetx=0, 
			fnety = FG;  //fy starts out being 9.8 anyways


			for(Grav gr: currentLevel.gravs){
				gr.update(player);
				fnetx+= gr.fx;
				fnety+= gr.fy;
			}



			//BM: Apply gravitation -> displacement to the player
			player.vel.x += (fnetx*(1.0/FRAMERATE)); // Apply gravity force, assuming unit mass.
			player.vel.y += (fnety*(1.0/FRAMERATE)); // ditto

			//	Integrate the velocity to find the new position
			//  also, updates player rectangle
			player.moveCenterTo( m2px( px2m(player.ctr.x) + player.vel.x * (1.0/FRAMERATE)), m2px(px2m(player.ctr.y) + player.vel.y * (1.0/FRAMERATE)));


			for(Turret turr: currentLevel.turrets){
				turr.update( player );  //the turrets need to know where the player is each frame.

				Bullet[]staticBullets = turr.bullets.toArray(blankBulletArray);
				for(int i= 0; i<staticBullets.length; i++){

					//Remove bullets that hit the level's platforms
					for(Integer ndx: currentLevel.adjPlace(staticBullets[i].getPointLocation().x, staticBullets[i].getPointLocation().y)){
						if( currentLevel.tileCollisionRectangles[ndx].contains(staticBullets[i].getPointLocation())){
							staticBullets[i].toDelete = true;
						}

					}

					//Remove bullets if/when they hit a laserwalls, or mps.
					for(LaserWall lw: currentLevel.laserWalls){
						if(lw.isOn && lw.getRect().contains(staticBullets[i].getPointLocation()))
							staticBullets[i].toDelete = true;
					}

					for(MovingPlatform mp: currentLevel.movingPlatforms){
						if( mp.getRect().contains(staticBullets[i].getPointLocation())){
							staticBullets[i].toDelete = true;
						}
					}

					if( staticBullets[i].toDelete){
						turr.bullets.remove(staticBullets[i]);
					}else{

						//calculate force from each gravwell/push on each bullet
						double bfx = 0, bfy = 0; //bullet force x, bullet force y --> net force from gravwells

						for(Grav gr: currentLevel.gravs){
							double[] bfs = gr.calcForceOn(staticBullets[i].getPointLocation());
							bfx+= bfs[0];
							bfy+= bfs[1];
						}


						staticBullets[i].update(player, bfx,bfy);

						//check for being killed by the bullet

						if(player.getRect().contains(staticBullets[i].getPointLocation())){
							killPlayer();
							break;
						}

					}

				}

			}


			//BM: Check for collisions with any tiles in the vicinity
			tilesToCheck = currentLevel.adjPlace(player.ctr.x, player.ctr.y);
			for(Integer i: tilesToCheck){
				if( currentLevel.tileCollisionRectangles[i].intersects(player.getRect()) ){
					killPlayer();
					break;
				}
			}


			if( //Check for player going out of bounds
					! (	(0<=player.ctr.x) && (player.ctr.x<=WIDTH) ) ||  
					! (	(0<=player.ctr.y) && (player.ctr.y<= HEIGHT-80) ) //height-80 b/c the height includes the bottombar
			){
				killPlayer();
			}


			//TODO: check for collisions with laserwalls
			for(LaserWall lw: currentLevel.laserWalls){
				lw.update();

				if( lw.isOn && player.getRect().intersects(lw.getRect())){
					killPlayer();
					break;
				}
			}

			//BM: check for collisions with moving platforms
			for(MovingPlatform mp: currentLevel.movingPlatforms){
				mp.update();
				if(player.getRect().intersects(mp.getRect())){
					killPlayer();
					break;
				}
			}




			//BM: Check for reaching the exit! :) 
			if( exitRect.intersects(player.getRect())){
				loadNextLevel();
			}

			//BM: Check for collisions with draws
			double[]cres; //collision results


			//			localizedDraws = draws.toArray(new Draw[0]);

			for(Draw draw: drawManager.getSnapshot()){
				if( draw == null) continue;

				if( pro_bouncing){
					cres = PhysicsMethods.collisionStuffPro(player.getRect(),draw, player.vel);
					if(cres[4]==1){ // A collision event took place...

						drawManager.getRidOf(draw);
						//Displace player
						player.moveCenterTo(player.ctr.x+ cres[0], player.ctr.y + cres[1]);

						player.vel.x = (cres[2]);
						player.vel.y = (cres[3]);
					}		

				}else{
					cres = PhysicsMethods.collisionStuffNoob(player.getRect(),draw, player.vel);

					if(cres[4]==1){ // A collision event took place...

						drawManager.getRidOf(draw);
						//Displace player
						player.moveCenterTo(player.ctr.x+ cres[0], player.ctr.y + cres[1]);
						player.vel.x = px2m(cres[2]);
						player.vel.y = px2m(cres[3]);
					}		

				}


			}

		}	
		public void uS_eog(){
		}


		/*** 
		 * PAINT METHODS  ******************************************
		 ***/

		public void paint(Graphics g){

			switch(garbooMode){
			case INGAME:
				paintGame(g);
				break;
			case MAINMENU:
				paintMenu(g);
				break;
			case CREDITS:
				paintCredits(g);
				break;
			case OPTIONS:
				paintOptions(g);
				break;
			case PAUSE:
				paintPause(g);
				break;
			case ENDOFGAME:
				paintEndOfGame(g);
				break;
			}	
		}


		public void paintMenu(Graphics g){
			g.drawImage(menu_img, 0, 0, null);
			//Draw selector image level with the menu choices
			g.drawImage( 
					selector_img.getNextFrame(),
					menu_start_x ,
					menu_start_y + menuSelection*menu_item_dy - (int)Math.round(selector_img.getFrameHalfHeight()),
					null );

		}
		public void paintCredits(Graphics g){

			if(scrollingCredits){
				if( creditsScrollY < 0 ){ //wait before starting to scroll
					creditsScrollY++;
					g.drawImage( credits_img, 0, 0, null); //draw the image statically
				}else{
					if( creditsScrollY < credits_img.getHeight(null) - HEIGHT){ //scroll
						creditsScrollY += scrollPixelsPerFrame;
					}else{
						scrollingCredits = false; //stop scrolling once we reach the limit
					}
					g.drawImage( credits_img, 0, -creditsScrollY, null); //but regardless, draw the thing
				}
			}

			//Make top-bottom letter boxes
			g.setColor(Color.black);
			g.fillRect(0, 0, 1280, 150);

			g.setColor(Color.black);
			g.fillRect(0, HEIGHT - 150, 1280, HEIGHT);


		}
		public void paintOptions(Graphics g){
			g.drawImage(options_img, 0, 0, null);			


			//Draw various combinations of selected/unselected items

			if( high_graphics){
				g.drawImage( 
						selected_img,
						high.selectorLocation.x - (int)(selected_img.getWidth(null)/2.0),
						high.selectorLocation.y - (int)(selected_img.getHeight(null)/2.0),
						null
				);

				g.drawImage( 
						unselected_img,
						low.selectorLocation.x - (int)(unselected_img.getWidth(null)/2.0),
						low.selectorLocation.y - (int)(unselected_img.getHeight(null)/2.0),
						null
				);
			}else{

				g.drawImage( 
						unselected_img,
						high.selectorLocation.x - (int)(unselected_img.getWidth(null)/2.0),
						high.selectorLocation.y - (int)(unselected_img.getHeight(null)/2.0),
						null
				);

				g.drawImage( 
						selected_img,
						low.selectorLocation.x - (int)(selected_img.getWidth(null)/2.0),
						low.selectorLocation.y - (int)(selected_img.getHeight(null)/2.0),
						null
				);
			}



			if( pro_bouncing){
				g.drawImage( 
						selected_img,
						pro.selectorLocation.x - (int)(selected_img.getWidth(null)/2.0),
						pro.selectorLocation.y - (int)(selected_img.getHeight(null)/2.0),
						null
				);

				g.drawImage( 
						unselected_img,
						noob.selectorLocation.x - (int)(unselected_img.getWidth(null)/2.0),
						noob.selectorLocation.y - (int)(unselected_img.getHeight(null)/2.0),
						null
				);

			}else{
				g.drawImage( 
						unselected_img,
						pro.selectorLocation.x - (int)(unselected_img.getWidth(null)/2.0),
						pro.selectorLocation.y - (int)(unselected_img.getHeight(null)/2.0),
						null
				);

				g.drawImage( 
						selected_img,
						noob.selectorLocation.x - (int)(selected_img.getWidth(null)/2.0),
						noob.selectorLocation.y - (int)(selected_img.getHeight(null)/2.0),
						null
				);
			}



			//Draw currently-selected-option pointer
			Point currSelection = optionsMenu.currNode.getSelectorLocation();

			g.drawImage( 
					selector_img.getNextFrame(),
					currSelection.x - selector_img.getFrameHalfWidth(),
					currSelection.y - selector_img.getFrameHalfHeight() ,
					null
			);



		}

		public void paintPause(Graphics g){
			if( high_graphics){
				paintPauseHIGH(g);
			}else{
				paintPauseLOW(g);
			}

		}
		public void paintPauseLOW(Graphics g){

			paintGame(g);

			//Draw the opaque gray box
			g.setColor(Color.white);
			g.setFont(gFont_big.deriveFont(30f));


			//redraw the label @ the bottom so that its not blurred out
			if( playingTrainingLevels){
				g.drawString( "training", 30, 840);
			}else{
				g.drawString( "L"+currentLevel.numeric_num+":  "+currentLevel.lname+" <"+currentLevel.lnum+">", 30, 840);
			}


			g.setColor(Color.black);

			switch(pauseMode){
			case USER_PAUSE:

				g.setFont(gFont_big);
				g.drawString("GAME PAUSED", 415, 360);

				g.setFont(gFont_small);
				g.drawString(" press >>esc<< to quit to main menu", 432, 430);
				g.drawString(" Click to resume game", 502, 460);

				g.drawString("Be sure to make note of the level code, so you can return here", 243, 550);
				g.drawString("without having to play through all the previous levels again", 253,580 );

				break;

			case CLICK_TO_BEGIN:
				g.setFont(gFont_big);
				g.drawString("CLICK TO BEGIN", 395, 360);

				g.setFont(gFont_small);
				g.drawString(" press >>esc<< to quit to main menu", 432, 430);

				g.drawString("Be sure to make note of the level code, so you can return here", 243, 550);
				g.drawString("without having to play through all the previous levels again", 253,580 );


				break;
			case CLICK_TO_RESPAWN:

				g.setFont(gFont_big);
				g.drawString("CLICK TO RESPAWN", 327, 360);

				g.setFont(gFont_small);
				g.drawString(" press >>esc<< to quit to main menu", 432, 430);

				g.drawString("Be sure to make note of the level code, so you can return here", 243, 550);
				g.drawString("without having to play through all the previous levels again", 253,580 );

				break;
			}

		}
		public void paintPauseHIGH(Graphics g){

			//			long nantime = System.nanoTime();

			//Apply 'frosted glass' aka gaussian blur effect to the pause screen.
			Graphics2D gb = (Graphics2D)blurredPause.getGraphics();
			paintGame(gb);

			blurredPause = GaussianBlur.changeImageWidth(blurredPause, blurredPause.getWidth() / 2);
			blurredPause = GaussianBlur.getGaussianBlurFilter(4/4 , true).filter(blurredPause, null);
			blurredPause = GaussianBlur.getGaussianBlurFilter(4/4, false).filter(blurredPause, null);
			blurredPause = GaussianBlur.changeImageWidth(blurredPause, blurredPause.getWidth() * 2);


			g.drawImage(blurredPause,0,0,null);

			//Draw the opaque gray box
			g.setColor(pauseScreenBGColor);
			g.fillRect(0,0,WIDTH, HEIGHT-80);

			g.setColor(Color.black);
			g.fillRect(0, 800 , WIDTH, 80);
			g.setColor(Color.white);
			g.setFont(gFont_big.deriveFont(30f));


			//redraw the label @ the bottom so that its not blurred out
			if( playingTrainingLevels){
				g.drawString( "training", 30, 840);
			}else{
				g.drawString( "L"+currentLevel.numeric_num+":  "+currentLevel.lname+" <"+currentLevel.lnum+">", 30, 840);
			}


			g.setColor(Color.WHITE);

			switch(pauseMode){
			case USER_PAUSE:

				g.setFont(gFont_big);
				g.drawString("GAME PAUSED", 415, 360);

				g.setFont(gFont_small);
				g.drawString(" press >>esc<< to quit to main menu", 432, 430);
				g.drawString(" Click to resume game", 502, 460);

				g.drawString("Be sure to make note of the level code, so you can return here", 243, 550);
				g.drawString("without having to play through all the previous levels again", 253,580 );

				break;

			case CLICK_TO_BEGIN:
				g.setFont(gFont_big);
				g.drawString("CLICK TO BEGIN", 395, 360);

				g.setFont(gFont_small);
				g.drawString(" press >>esc<< to quit to main menu", 432, 430);

				g.drawString("Be sure to make note of the level code, so you can return here", 243, 550);
				g.drawString("without having to play through all the previous levels again", 253,580 );


				break;
			case CLICK_TO_RESPAWN:

				g.setFont(gFont_big);
				g.drawString("CLICK TO RESPAWN", 327, 360);

				g.setFont(gFont_small);
				g.drawString(" press >>esc<< to quit to main menu", 432, 430);

				g.drawString("Be sure to make note of the level code, so you can return here", 243, 550);
				g.drawString("without having to play through all the previous levels again", 253,580 );

				break;
			}

		}

		public void paintGame(Graphics g){
			if( high_graphics){
				paintGameHIGH(g);
			}else{
				paintGameLOW(g);
			}
		}
		public void paintEndOfGame(Graphics g){

			paintGame(g);

			g.setColor(endOfGameBGColor);
			g.fillRect(0,0,WIDTH, HEIGHT-80);
			g.setColor(Color.WHITE);

			if(playingTrainingLevels){
				g.drawImage(endofgame_training,0,0,null);
			}else{
				g.drawImage(endofgame,0,0,null);
			}
		}
		public void paintGameHIGH(Graphics g){

			Graphics2D g2 = (Graphics2D)g;

			//BM: Paint bg image (custom drawn for each level) and black bottom bar
			g2.drawImage(currentLevel.levelBackground, 0, 0, null);

			//Make stuff look good:
			g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
			g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

			//BM: Draw bottom bar and level text stuff:
			g2.setColor(Color.black);
			g2.fillRect(0, 800 , WIDTH, 80);
			g2.setColor(Color.white);
			g2.setFont(gFont_big.deriveFont(30f));

			if( playingTrainingLevels){
				g2.drawString( "training", 30, 840);
			}else{
				g2.drawString( "L"+currentLevel.numeric_num+":  "+currentLevel.lname+" <"+currentLevel.lnum+">", 30, 840);
			}

			//BM: Draw temp start and end and start points

			g2.setColor(Color.green);
			g2.fill(exitRect);


			//TODO: Draw moving platforms
			for( MovingPlatform mp: currentLevel.movingPlatforms){
				g2.drawImage( (mp.getOrient()==0)?mp_horiz:mp_vert, mp.getDrawX(), mp.getDrawY(),null);
			}

			//BM: Draw laser walls

			for( LaserWall lw: currentLevel.laserWalls){
				if( lw.isOn){
					g2.drawImage(laserimgs.get((int)(Math.random()*4)), lw.loc.x*40, lw.loc.y*40,null);
				}
				else{
					g2.setColor( laserWallColor_off);
					g2.fill(lw.getRect());
				}

			}


			//BM: Draw all turrets


			for( Turret turr: currentLevel.turrets){

				for( Bullet b: turr.bullets.toArray(new Bullet[0])){
					g2.translate( b.pos.x, b.pos.y);
					g2.rotate(b.angle);
					//					g2.drawImage(bullet_img,b.getPointLocation().x - 2, b.getPointLocation().y-2,null);				
					g2.drawImage(bullet_img,-(int)(bullet_img.getWidth(null)/2.0), -(int)(bullet_img.getHeight(null)/2.0),null);

					g2.rotate(-b.angle);
					g2.translate( -b.pos.x, -b.pos.y);

				}


				g2.translate(turr.getCenterPoint().x , turr.getCenterPoint().y);

				g2.rotate(turr.getAngle());
				if(turr.isGreen){
					g2.drawImage(turret_green, -20, -20, null);
				}else{
					g2.drawImage(turret_red, -20, -20, null);					
				}
				g2.rotate(-turr.getAngle());

				g2.translate( -turr.getCenterPoint().x, -turr.getCenterPoint().y);				

			}




			//draw gravs:
			for(Grav gr: currentLevel.gravs){	

				g2.translate( gr.center.x, gr.center.y);
				g2.rotate( gr.angle);

				switch(gr.pull){
				case 1: //gravpulls

					switch(gr.gs){
					case _40:
						g2.drawImage( gpull40_img, - (int)(gpull40_img.getWidth(null)/2.0),- (int)(gpull40_img.getHeight(null)/2.0),null);
						break;
					case _80:
						g2.drawImage( gpull80_img, - (int)(gpull80_img.getWidth(null)/2.0),- (int)(gpull80_img.getHeight(null)/2.0),null);
						break;
					case _120:
						g2.drawImage( gpull120_img, - (int)(gpull120_img.getWidth(null)/2.0),- (int)(gpull120_img.getHeight(null)/2.0),null);
						break;
					case _160:
						g2.drawImage( gpull160_img, - (int)(gpull160_img.getWidth(null)/2.0),- (int)(gpull160_img.getHeight(null)/2.0),null);
						break;
					case _200:
						g2.drawImage( gpull200_img, - (int)(gpull200_img.getWidth(null)/2.0),- (int)(gpull200_img.getHeight(null)/2.0),null);
						break;
					}


					break;
				case 0: //gravpushes

					switch(gr.gs){
					case _40:
						g2.drawImage( gpush40_img, - (int)(gpush40_img.getWidth(null)/2.0),- (int)(gpush40_img.getHeight(null)/2.0),null);
						break;
					case _80:
						g2.drawImage( gpush80_img, - (int)(gpush80_img.getWidth(null)/2.0),- (int)(gpush80_img.getHeight(null)/2.0),null);
						break;
					case _120:
						g2.drawImage( gpush120_img, - (int)(gpush120_img.getWidth(null)/2.0),- (int)(gpush120_img.getHeight(null)/2.0),null);
						break;
					case _160:
						g2.drawImage( gpush160_img, - (int)(gpush160_img.getWidth(null)/2.0),- (int)(gpush160_img.getHeight(null)/2.0),null);
						break;
					case _200:
						g2.drawImage( gpush200_img, - (int)(gpush200_img.getWidth(null)/2.0),- (int)(gpush200_img.getHeight(null)/2.0),null);
						break;
					}

					break;
				}





				g2.rotate( -gr.angle);
				g2.translate( -gr.center.x, -gr.center.y);

			}



			//BM: Draw player
			if(player.isAlive()){
				g2.drawImage( (player.vel.x<0)?player_img_L:player_img_R, player.getRect().x, player.getRect().y, null);
			}
			else{
				g2.drawImage( splatAnim.getNextFrame(),
						player.getRoundedCenter().x - splatAnim.getFrameHalfWidth(),
						player.getRoundedCenter().y - splatAnim.getFrameHalfHeight(), null);
			}

			//We need to make a local copy JIC there is a deletion while we're iterating over the list.

			for(Draw d:drawManager.getSnapshot()){
				if( d == null) continue;
				d.drawSelf(g2);
			}

		}

		public void paintGameLOW(Graphics g){

			Graphics2D g2 = (Graphics2D)g;
			g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);


			g2.clearRect(0,0,WIDTH, HEIGHT);

			//BM: Draw bottom bar and level text stuff:
			g2.setColor(Color.black);
			g2.fillRect(0, 800 , WIDTH, 80);
			g2.setColor(Color.white);
			g2.setFont(gFont_big.deriveFont(30f));

			if( playingTrainingLevels){
				g2.drawString( "training", 30, 840);
			}else{
				g2.drawString( "L"+currentLevel.numeric_num+":  "+currentLevel.lname+" <"+currentLevel.lnum+">", 30, 840);
			}

			//BM: draw temporary tiles

			for( Rectangle r: currentLevel.tileCollisionRectangles){
				g2.setColor(Color.black);
				g2.draw(r);
			}

			//BM: Draw temp start and end and start points
			g2.setColor(Color.blue);
			g2.drawRect((int)currentLevel.playerstart.x-5, (int)currentLevel.playerstart.y-5 , 10, 10);
			g2.setColor(Color.green);
			g2.draw(exitRect);

			g2.setColor(Color.darkGray);
			//TODO: Draw moving platforms
			for( MovingPlatform mp: currentLevel.movingPlatforms){
				g2.draw(mp.getRect());
			}

			//BM: Draw laser walls
			for( LaserWall lw: currentLevel.laserWalls){
				if( lw.isOn){
					g2.setColor(Color.red);
				}
				else{
					g2.setColor( Color.LIGHT_GRAY);
				}
				g2.draw(lw.getRect());
			}


			//BM: Draw all turrets

			Ellipse2D.Double turrEllipse = new Ellipse2D.Double(-20, -20, 40, 40);

			for( Turret turr: currentLevel.turrets){

				for( Bullet b: turr.bullets.toArray(new Bullet[0])){
					g2.drawImage(bullet_img,b.getPointLocation().x - 2, b.getPointLocation().y-2,null);				
				}

				g2.translate(turr.getCenterPoint().x , turr.getCenterPoint().y);
				if(turr.isGreen){
					g2.setColor(Color.green);
				}else{
					g2.setColor(Color.red);					
				}
				g2.draw(turrEllipse);
				g2.drawRect( (int)(20*Math.sin(turr.angle)), (int)(-20*Math.cos(turr.angle)), 2, 2);


				g2.translate( -turr.getCenterPoint().x, -turr.getCenterPoint().y);				

			}

			//draw gravs:
			for(Grav gr: currentLevel.gravs){	
				g2.setColor(Color.yellow);
				g2.drawOval(gr.center.x - gr.activationR, gr.center.y - gr.activationR, 2*gr.activationR, 2*gr.activationR);
				if(gr.actingOnPlayer){
					g2.setColor(Color.pink);
				}else{
					g2.setColor(Color.blue);
				}
				g2.drawRect(gr.center.x -20, gr.center.y-20, 40, 40);

			}

			//BM: Draw player
			g2.setColor(Color.DARK_GRAY);
			g2.draw(player.getRect());

			//We need to make a local copy JIC there is a deletion while we're iterating over the list.
			for(Draw d:drawManager.getSnapshot()){
				if( d == null) continue;
				d.drawSelf(g2);
			}

		}


	}


	/******/

	/** This method will prepare the level specified by
	 *  loading the definition into currentLevel, clearing
	 *  and allocating an array for Draw objects, and 
	 *  spawning a Player object at the PlayerStart position
	 *  specified in the level definiton.
	 * @param lvl The level to be loaded. Do not prefix
	 *  with level's path, just provide the name.
	 */
	public final void prepareLevel(String lvl) {

		System.out.println("Prepping:"+ LEVELDIR + lvl);

		//Load level, initialize player and an array for keeping track of draws
		currentLevel = new Level(LEVELDIR + lvl, FRAMERATE); //test level
		exitRect = new Rectangle(
				(int)currentLevel.playerexit.x - 30,
				(int)currentLevel.playerexit.y - 30,
				60,60);

		drawManager = new DrawManager(); //Make a new draw manager each level

		//Note: the player-class constructor uses clones of the passed-in Point2D.Doubles instead of the references passed in
		player = new Player(currentLevel.playerstart,PWIDTH,PHEIGHT,currentLevel.playervel);

	}
	public final void quickReset() {

		//Reset the player to their old position.
		player = new Player(currentLevel.playerstart,PWIDTH,PHEIGHT,currentLevel.playervel); //player starts out alive, too

		//Reset draws and all
		drawManager.reset();

		//		drawManager.addNewDraw(new Point(10, 300));
		//		drawManager.dragTo(new Point( 400, 300));
		//		
		////		drawManager.addNewDraw(new Point(400, 100));
		////		drawManager.dragTo(new Point( 400, 300));

		drawManager.releaseDragging();


		//Reset the state of all things on-screen
		for(MovingPlatform mp: currentLevel.movingPlatforms)
			mp.reset();
		for(LaserWall lw: currentLevel.laserWalls)
			lw.reset();
		for(Turret turr : currentLevel.turrets){
			turr.reset();
		}

	}
	public final void killPlayer(){
		splatAnim.reset(); //set the splat animation to start from frame 0 again

		//Actually mark player as dead
		player.setAlive(false);

		pauseMode = PauseMode.CLICK_TO_RESPAWN;
		garbooMode = Mode.PAUSE;

		setListeners( pListener);
	}
	public final void loadNextLevel() {

		//If there is a next level, load it, otherwise... TBD
		if(currentLevel.nextLevel!=null){

			prepareLevel( currentLevel.nextLevel);

			pauseMode = PauseMode.CLICK_TO_BEGIN;
			garbooMode = Mode.PAUSE;

			setListeners(pListener);
		}else{

			if( !funlevel){
				garbooMode = Mode.ENDOFGAME;
				setListeners(eogListener);
			}else{
				garbooMode = Mode.MAINMENU;
				setListeners(mListener);
				
			}

		}
	}


	/******/

	public final void setListeners(GenericListener o) {

		for(MouseListener l:getMouseListeners()) removeMouseListener(l);
		for(KeyListener l:getKeyListeners()) removeKeyListener(l);
		for(MouseMotionListener l:getMouseMotionListeners()) removeMouseMotionListener(l);

		addMouseListener(o);
		addMouseMotionListener(o);
		addKeyListener(o);

	}

	/******/

	public static double m2px(double m){
		return m*PIXELS_PER_METER;
	}
	public static double px2m(double px){
		return px/PIXELS_PER_METER;
	}

	/******/

	public static void main(String[]args){new GarbooMain();
	}

	/******/

	/******/

	public class CodeFinder implements Runnable{
		private boolean codeFound;
		private boolean doneSearching;
		private String level;
		private String code;

		public CodeFinder(String code){
			codeFound = false;
			doneSearching = false;
			this.code = code;
		}
		public final void run() {

			for(int i = 0; i<10;i++){ //1-4
				for(int j = 1;j<=10;j++){ // 1-5
					System.out.println("Looking in: "+LEVELDIR+i+""+j);
					ArrayList<String> fileText = new ArrayList<String>();
					try {
						//Load in level data into an arraylist for tag-searching
						Scanner fin = new Scanner(new FileReader(LEVELDIR + i+""+j));
						while(fin.hasNextLine()){
							fileText.add(fin.nextLine());

							//Remove comments and blank lines from file text
							if(fileText.get(fileText.size()-1).equals("") ||
									fileText.get(fileText.size()-1).startsWith("//"))

								fileText.remove(fileText.size()-1);
						}
					} catch (FileNotFoundException e) {
						System.err.println("Unable to read level definition!");
						e.printStackTrace();			
					}
					//Look for a LNUM tag...
					String lnum = null;

					for(int k = 0; k < fileText.size(); k++	){
						String line = fileText.get(k).trim();	
						switch(line.charAt(0)){
						case 'L':
							if(line.substring(0, line.indexOf(" ")).trim().equals("LNUM")){
								//								System.out.println("FOUND LEVEL NAME TAG:"+line);
								lnum = line.substring(line.indexOf(" ")).trim();
							}else{
								//								System.out.println("L TAG WAS NOT FOR LEVEL NAME:"+line);
							}
							if(lnum!=null && lnum.equals(code)){ //This is our match!
								codeFound = true;
								doneSearching = true;
								level = i+""+j;
								System.out.println("Found the match: "+lnum+" matches the given code, "+code);
								return;
							}
							break;
						}
					}
					System.out.println("Finished looking at :"+LEVELDIR + i + ""+j);
				}
			}
			doneSearching = true;

		}
		public String getFoundLevel(){
			return level;
		}
		public boolean isDoneSearching(){
			return doneSearching;
		}
		public boolean isCodeFound(){
			return codeFound;
		}

	}

	public class PauseListeners extends GenericListener{
		public final void keyPressed(KeyEvent e){
			System.out.println("KeyPressed! [PauseListener]");

			//regardless, escape is always quit so, do so
			if(e.getKeyCode() == KeyEvent.VK_ESCAPE){

				garbooMode = Mode.MAINMENU;
				setListeners(mListener);
				playingTrainingLevels = false; //just in case
				funlevel = false;
				//force a respawn? actually, not needed, because
				// 'start a new game' will autoforce a respawn
			}
		}
		public final void mousePressed(MouseEvent e){
			//			if(pauseMode == PauseMode.CLICK_TO_BEGIN || pauseMode == PauseMode.CLICK_TO_RESPAWN || pauseMode = PauseMode.USER_PAUSE){
			// ^-- this is stupid b/c that's all the possible modes so...

			quickReset(); 
			garbooMode = Mode.INGAME;
			setListeners(igListener);

		}

	}

	public class InGameListeners extends GenericListener{
		boolean btn1 = false; //flag for determining if its a left-click drag

		public final void mousePressed(MouseEvent e) {
			if(e.getButton() == MouseEvent.BUTTON1){

				Point p = e.getPoint(); //Correct the offsets due to borders...
				p.x-=xoff; p.y-=yoff;

				drawManager.addNewDraw(p);


				btn1 = true;
			}


		}
		public final void mouseDragged(MouseEvent e) {

			if(btn1){
				Point p = e.getPoint();
				p.x-=xoff; p.y-=yoff;

				//Try to drag... who knows if something actually exists,
				//drawmanager is responsible for that
				drawManager.dragTo(p);

			}
		}

		public final void mouseReleased(MouseEvent e){
			//Let go of whatever we may have been dragging
			//( could have been nothing, if our drag was destroyed by a bounce b4 we let go)
			drawManager.releaseDragging();
		}



		public final void keyPressed(KeyEvent e){

			if(e.getKeyCode() == KeyEvent.VK_P || e.getKeyCode() == KeyEvent.VK_ESCAPE){

				pauseMode = PauseMode.USER_PAUSE;
				garbooMode = Mode.PAUSE;
				setListeners(pListener);

			}else if(e.getKeyCode() == KeyEvent.VK_K){
				killPlayer();
			}

		}
	}

	public class MenuListeners extends GenericListener{
		public final void keyPressed(KeyEvent e) {

			if(e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_RIGHT){
				menuSelection = (menuSelection+1)%menuItems;

			}else if(e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_LEFT){
				menuSelection = ( menuSelection == 0)?menuItems-1: (menuSelection-1);

			}else if(e.getKeyCode() == KeyEvent.VK_ESCAPE){
				System.exit(0);
			}

			else if( e.getKeyCode() == KeyEvent.VK_ENTER){
				if( menuSelection == 0){

					/**/
					playingTrainingLevels = true;



					prepareLevel(FIRSTTRAININGLEVEL);

					pauseMode = PauseMode.CLICK_TO_BEGIN;
					garbooMode = Mode.PAUSE;
					setListeners( pListener);




				}	else if( menuSelection == 1){

					prepareLevel(FIRSTLEVEL);

					pauseMode = PauseMode.CLICK_TO_BEGIN;
					garbooMode = Mode.PAUSE;
					setListeners( pListener);

				}else if(menuSelection == 2){ //entered code
					setAlwaysOnTop(false);

					String enteredCode = JOptionPane.showInputDialog("What's this code you speak of?");
					if( enteredCode!=null){

						CodeFinder codeFinder = new CodeFinder(enteredCode);
						codeFinder.run();

						while(!codeFinder.isDoneSearching()); //wait for codefinder to finish searching

						if(codeFinder.isCodeFound()){

							if( codeFinder.getFoundLevel().startsWith("0")){
								playingTrainingLevels = true;
							}else if( codeFinder.getFoundLevel().startsWith("9")){
								funlevel = true;

							}

							prepareLevel(codeFinder.getFoundLevel());
							pauseMode = PauseMode.CLICK_TO_BEGIN;
							garbooMode = Mode.PAUSE;
							setListeners( pListener);

						}else{
							JOptionPane.showMessageDialog(null,"Make sure you have the level code right. No levels with the code you specified were found.");
						}
					}

					setAlwaysOnTop(true);

				}else if(menuSelection == 3){

					garbooMode = Mode.OPTIONS;
					setListeners( oListener);


				}else if(menuSelection == 4){ //Credits

					setListeners(cListener);
					garbooMode = Mode.CREDITS;
					scrollingCredits= true;
					creditsScrollY = - FRAMERATE * scrollPixelsPerFrame;

				}else if(menuSelection == 5){ //exit!
					System.exit(0);
				}

			}
		}
	}

	public class CreditsScreenListeners extends GenericListener{
		public final void keyPressed(KeyEvent e) {
			if(e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_ESCAPE ){
				setListeners(mListener);
				garbooMode = Mode.MAINMENU;
				scrollingCredits = false;

			}
		}
	}

	public class OptionsScreenListeners extends GenericListener{
		public final void keyPressed(KeyEvent e) {

			if(e.getKeyCode() == KeyEvent.VK_ENTER){

				if( optionsMenu.getCurrentNode().getName().equals("pro")){
					pro_bouncing = true;
				}else if( optionsMenu.getCurrentNode().getName().equals("noob")){
					pro_bouncing = false;					
				}else if( optionsMenu.getCurrentNode().getName().equals("low")){
					high_graphics = false;
				}else if( optionsMenu.getCurrentNode().getName().equals("high")){
					high_graphics = true;

				}else if( optionsMenu.getCurrentNode().getName().equals("menu")){
					setListeners(mListener);
					garbooMode = Mode.MAINMENU;
					//reset position of selector for next time
					//we use the menu
					optionsMenu.setCurrentNode(optionsMenu.getHeadNode());
				}

			}else if( e.getKeyCode() == KeyEvent.VK_RIGHT){

				optionsMenu.moveToRight();
			}else if( e.getKeyCode() == KeyEvent.VK_LEFT){
				optionsMenu.moveToLeft();

			}else if( e.getKeyCode() == KeyEvent.VK_UP){
				optionsMenu.moveToUp();				

			}else if( e.getKeyCode() == KeyEvent.VK_DOWN){
				optionsMenu.moveToDown();

			}else if( e.getKeyCode() == KeyEvent.VK_ESCAPE){
				setListeners(mListener);
				garbooMode = Mode.MAINMENU;
				//reset position of selector for next time
				//we use the menu
				optionsMenu.setCurrentNode(optionsMenu.getHeadNode());
			}


			System.out.println(optionsMenu.getCurrentNode().toString());
		}
	}


	public class EndOfGameListener extends GenericListener{
		public final void keyPressed(KeyEvent e) {
			if(e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_ESCAPE ){
				if(playingTrainingLevels){
					playingTrainingLevels = false;
					setListeners(mListener);
					garbooMode = Mode.MAINMENU;

				} else {
					setListeners(cListener);
					garbooMode = Mode.CREDITS;

				}
			}
		}
		public final void mousePressed( MouseEvent e){

			if(playingTrainingLevels){
				playingTrainingLevels = false;
				setListeners(mListener);
				garbooMode = Mode.MAINMENU;

			} else {
				setListeners(cListener);
				garbooMode = Mode.CREDITS;

			}

		}



	}

	public class GenericListener  implements KeyListener, MouseListener, MouseMotionListener{

		public void mouseDragged(MouseEvent e) {
			// TODO Auto-generated method stub

		}
		public void mouseMoved(MouseEvent e) {
			// TODO Auto-generated method stub

		}
		public void mouseClicked(MouseEvent e) {
			// TODO Auto-generated method stub

		}
		public void mouseEntered(MouseEvent e) {
			// TODO Auto-generated method stub

		}
		public void mouseExited(MouseEvent e) {
			// TODO Auto-generated method stub

		}
		public void mousePressed(MouseEvent e) {
			// TODO Auto-generated method stub

		}
		public void mouseReleased(MouseEvent e) {
			// TODO Auto-generated method stub

		}
		public void keyPressed(KeyEvent arg0) {
			// TODO Auto-generated method stub

		}
		public void keyReleased(KeyEvent arg0) {
			// TODO Auto-generated method stub

		}
		public void keyTyped(KeyEvent arg0) {
			// TODO Auto-generated method stub

		}

	}

}